/*
 * Copyright 2009 Yannick Stucki (yannickstucki.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.yannickstucki.android.musicqueue;

import java.io.IOException;

import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.os.Binder;
import android.os.IBinder;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.view.KeyEvent;

/**
 * The service that runs in the background and plays the music.
 * 
 * @author Yannick Stucki (yannickstucki@gmail.com)
 * 
 */
public class PlayerService extends Service {

  /**
   * The maximal delay in milliseconds between two clicks on the head set which
   * still triggers a double click.
   */
  private static final int DOUBLE_CLICK_DELAY = 150;

  /**
   * TODO: Only temporary.
   */
  public boolean destroyed = false;



  /**
   * The song manager is needed so the service knows which song to play.
   */
  private SongManager songManager;

  /**
   * The currently playing song (either playing or on pause).
   */
  private Song nowPlaying;

  /**
   * Updates the progress bar.
   * 
   * TODO: why is this reference here? This is ui, should be controlled by
   * MusicQueue if poss1ible.
   */
  private ProgressUpdater progressUpdater;

  /**
   * The class that actually plays the songs.
   */
  private MediaPlayer mediaPlayer;

  /**
   * a reference to the UI class.
   */
  private MusicQueue musicQueue;

  /**
   * Somehow playPause is triggered while starting up and this shouldn't be
   * needed.
   * 
   * TODO: investigate
   */
  private boolean firstPlayPause = true;

  /**
   * If true, the head set got unplugged while the music was running. Therefore
   * the music has to resume when the head set is plugged in again.
   * 
   * TODO: when there's a call going on, the music shouldn't resume (or only
   * after the call?)
   */
  private boolean headSetUnplugged = false;

  /**
   * If this is true, the music should resume when the current call ends.
   */
  private boolean musicInterruptedByCall = false;

  /**
   * Handles the plugging / unplugging of the head set.
   */
  private HeadSetPlugReceiver headSetPlugReceiver = new HeadSetPlugReceiver();

  /**
   * Handles interrupting calls.
   */
  private MyPhoneStateListener phoneListener;

  /**
   * The shared preferences to receive whether the headphone button should be
   * used or not.
   */
  private SharedPreferences settings;

  @Override
  public void onRebind(Intent intent) {
    Logger.out("service on rebind");
    super.onRebind(intent);
  }

  @Override
  public void onStart(Intent intent, int startId) {
    Logger.out("service on start");
    super.onStart(intent, startId);
    settings = this.getSharedPreferences(MusicQueue.PREFERENCES_NAME,
        Activity.MODE_PRIVATE);
  }

  @Override
  public boolean onUnbind(Intent intent) {
    Logger.out("service on unbind");
    return super.onUnbind(intent);
  }

  /**
   * Registers the phone state listener so it receives events.
   */
  private TelephonyManager telephonyManager;

  /**
   * Handles media button clicks.
   */
  private MediaButtonReceiver mediaButtonReceiver;

  /**
   * Used to display notifications in the top bar.
   */
  private NotificationManager nm;

  /**
   * Binder between the Activity {@link MusicQueue} and this service.
   */
  private final IBinder binder = new LocalBinder();

  /**
   * Local binder implementation that returns this service.
   */
  public class LocalBinder extends Binder {

    /**
     * Returns this service.
     */
    PlayerService getService() {
      return PlayerService.this;
    }
  }

  @Override
  public void onCreate() {
    super.onCreate();
    Logger.out("PlayerService.onCreate");
    mediaButtonReceiver = new MediaButtonReceiver(this.getString(R.string.headset_key));
    mediaPlayer = new MediaPlayer();
    nm = (NotificationManager) this
        .getSystemService(Context.NOTIFICATION_SERVICE);
    this.setForeground(true);
    this.registerReceiver(headSetPlugReceiver, new IntentFilter(
        "android.intent.action.HEADSET_PLUG"));

    this.registerReceiver(mediaButtonReceiver, new IntentFilter(
        "android.intent.action.MEDIA_BUTTON"));

    phoneListener = new MyPhoneStateListener();
    telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
    telephonyManager
        .listen(phoneListener, PhoneStateListener.LISTEN_CALL_STATE);
  }

  @Override
  public void onDestroy() {
    super.onDestroy();
    this.destroyed = true;
    Logger.out("PlayerService.onDestroy");

    mediaPlayer.stop();
    mediaPlayer.release();
    stopProgressUpdater();

    nm.cancel(1);
    this.unregisterReceiver(headSetPlugReceiver);
    this.unregisterReceiver(mediaButtonReceiver);
    telephonyManager.listen(phoneListener, PhoneStateListener.LISTEN_NONE);
    if (musicQueue == null || musicQueue.destroyed) {
      songManager.close();
    }
  }

  // Service stuff
  @Override
  public IBinder onBind(Intent intent) {
    return binder;
  }

  /**
   * Sets the music queue reference. Should be called immediately after
   * obtaining this service.
   */
  public void setMusicQueue(MusicQueue musicQueue) {
    this.musicQueue = musicQueue;
  }

  /**
   * Toggles between play and pause.
   */
  public void playPause() {
    if (!mediaPlayer.isPlaying()) {
      mediaPlayer.start();
      showNotification(songManager.songs[0]);
      startProgressUpdater();
      if (!musicQueue.getLandscape()) {
        musicQueue.playerUiPortrait.setImageToPause();
      }
    } else {
      mediaPlayer.pause();
      stopProgressUpdater();
      nm.cancel(1); // TODO: what is this 1
      if (!musicQueue.getLandscape()) {
        musicQueue.playerUiPortrait.setImageToPlay();
      }
    }
  }

  /**
   * Called when the list is updated. This method starts playing a new song if
   * the top one changed
   */
  public void listUpdated() {
    if (songManager.songs[0] == null) {
      mediaPlayer.stop();
      return;
    }
    if (!songManager.songs[0].equals((nowPlaying))) {
      playSong();
    }
  }

  /**
   * Plays the top song.
   */
  private void playSong() {
    try {
      Song song = songManager.songs[0];
      
      try {
	mediaPlayer.reset();
      
      // TODO: Reusing MediaPlayer here seems to result in many crashes.
      //       To avoid this situation, just create new ones even if it is more expensive.
      //       This is definetely not the best thing to do but it fixes issue 32.
      mediaPlayer = new MediaPlayer();
          } catch (IllegalStateException e) {
        // TODO: does this happen anymore?
      }
      mediaPlayer.setDataSource(song.path);
      mediaPlayer.prepare();
      mediaPlayer.start();
      nowPlaying = song;
   
      if (!musicQueue.getLandscape()) {
        musicQueue.playerUiPortrait.setImageToPause();
      }

      int progress = musicQueue.getSavedProgress();
      if (progress > nowPlaying.duration) {
        progress = 0;
      }
      // TODO: this didn't cause the strange effect, can do it again
      // if (progress != 0) {
      // mediaPlayer.seekTo(progress);
      // }

      if (!musicQueue.getLandscape()) {
        musicQueue.playerUiPortrait.seekBar.setMax(song.duration);
        musicQueue.playerUiPortrait.seekBar.setProgress(progress);

        // update the song time
        int secs = song.duration / Helper.MILLISECONDS_IN_SECOND;
        int mins = secs / Helper.SECONDS_IN_MINUTE;
        secs -= mins * Helper.SECONDS_IN_MINUTE;
        musicQueue.playerUiPortrait.duration
            .setText(Helper.getTime(mins, secs));
        startProgressUpdater();
      }
      // Setup listener so next song starts automatically
      mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
        public void onCompletion(MediaPlayer arg0) {
          songManager.remove(0); // the top one
          nowPlaying = null;
        }
      });

      showNotification(song);

    } catch (IOException e) {
      Logger.out(e);
    }
  }

  /**
   * Seeks to a certain position in milliseconds within the song.
   */
  public void seekTo(int milliseconds) {
    mediaPlayer.seekTo(milliseconds);
  }

  /**
   * Returns the current position of the song in milliseconds.
   */
  public int getCurrentPosition() {
    return mediaPlayer.getCurrentPosition();
  }

  /**
   * Shows a notification of the new song being played / resumed at the top.
   */
  void showNotification(Song song) { // the notification at the top
    PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
        new Intent(this, MusicQueue.class), 0);

    Notification n = new Notification(R.drawable.play, "Playing " + song.title,
        System.currentTimeMillis());
    n.setLatestEventInfo(this, "Music Queue", song.title + " - " + song.artist,
        contentIntent);
    n.flags |= Notification.FLAG_ONGOING_EVENT;

    nm.notify(1, n);
  }

  /**
   * Starts the progress updater thread.
   */
  private void startProgressUpdater() {
    if (musicQueue != null) { // TODO: is it ever null?
      if (!musicQueue.getUpdateUi()) {
        Logger.debug("valid");
        return;
      }
    } else {
      return;
    }
    Logger.out("Starting Progress Updater");
    if (progressUpdater != null) {
      progressUpdater.requestStop();
    }
    progressUpdater = new ProgressUpdater(musicQueue, mediaPlayer);
    progressUpdater.start();
  }

  /**
   * Stops the progress updater thread.
   */
  public void stopProgressUpdater() {
    Logger.out("Stopping Progress Updater");
    if (progressUpdater != null) {
      progressUpdater.requestStop();
    }
  }

  /**
   * Called when the GUI is displayed again so the updates of the song playing
   * can resume.
   */
  public void guiResume() {
    if (musicQueue.getLandscape()) {
      return;
    }
    if (mediaPlayer.isPlaying()) {
      startProgressUpdater();
    }
    if (nowPlaying != null) {
      // update the song time
      int secs = nowPlaying.duration / Helper.MILLISECONDS_IN_SECOND;
      int mins = secs / Helper.SECONDS_IN_MINUTE;
      secs -= mins * Helper.SECONDS_IN_MINUTE;

      if (musicQueue.playerUiPortrait != null) {
        musicQueue.playerUiPortrait.seekBar.setMax(nowPlaying.duration);
        musicQueue.playerUiPortrait.duration
            .setText(Helper.getTime(mins, secs));
        musicQueue.playerUiPortrait.seekBar.setProgress(mediaPlayer
            .getCurrentPosition());
      }
    }
  }

  /**
   * Changes the number on the button on the bottom right.
   * 
   * TODO: this method belongs to MusicQueue.
   */
  public void updateQueueSizeButton(int waitingQueueSize) {
    if (musicQueue != null) {
      if (musicQueue.getLandscape()) {
        return;
      }
      if (musicQueue.playerUiPortrait != null) {
        musicQueue.playerUiPortrait.setQueueSize(waitingQueueSize);
      }
      if (waitingQueueSize == 0) {
        musicQueue.setClearQueueEnabled(false);
      } else {
        musicQueue.setClearQueueEnabled(true);
      }
    }
  }

  /**
   * Sets the reference of the song manager.
   */
  public void setSongManager(SongManager songManager) {
    this.songManager = songManager;
  }

  /**
   * Returns the reference of the song manager.
   */
  public SongManager getSongManager() {
    return songManager;
  }

  /**
   * True if the media player is currenlty playing.
   */
  public boolean isPlaying() {
    return mediaPlayer.isPlaying();
  }

  /**
   * Inner class for handling interruption on incoming phone calls.
   */
  private class MyPhoneStateListener extends PhoneStateListener {
    @Override
    public void onCallStateChanged(int state, String incomingNumber) {
      if (state == TelephonyManager.CALL_STATE_IDLE && musicInterruptedByCall
          && !mediaPlayer.isPlaying()) {
        musicInterruptedByCall = false;
        playPause();
      } else { // state ringing or off hook
        if (mediaPlayer.isPlaying()) {
          musicInterruptedByCall = true;
          playPause();
        }
      }
    }
  }

  /**
   * Inner class for handling the plugging and unplugging of the head set.
   */
  private class HeadSetPlugReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
      int state = intent.getExtras().getInt("state");
      if (firstPlayPause) {
        firstPlayPause = false;
      } else {
        if (mediaPlayer.isPlaying() && state == 0) {
          headSetUnplugged = true;
          playPause();
        }
        if (!mediaPlayer.isPlaying() && state == 1 && headSetUnplugged) {
          headSetUnplugged = false;
          playPause();
        }
      }
    }
  }

  /**
   * Inner class for handling the media button.
   */
  private class MediaButtonReceiver extends BroadcastReceiver {

    /**
     * Used to detect double clicks on the media button.
     */
    private long lastMediaButtonClick = System.currentTimeMillis();
    
    /**
     * Key to access the preferences.
     */
    private String headsetKey;
    
    /**
     * Constructor to set the head set key.
     */
    public MediaButtonReceiver(String headsetKey) {
      this.headsetKey = headsetKey;
    }
    
    @Override
    public void onReceive(Context context, Intent intent) {
      if (settings.getBoolean(headsetKey, false)) {
        KeyEvent keyEvent = (KeyEvent) intent.getExtras().get(
            "android.intent.extra.KEY_EVENT");
        if (keyEvent != null) {
          if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
            if (System.currentTimeMillis() - lastMediaButtonClick < DOUBLE_CLICK_DELAY) {
              songManager.remove(0);
            } else {
              playPause();
            }
          }
          lastMediaButtonClick = System.currentTimeMillis();
        }
        this.abortBroadcast();
      }
    }
  }
}
